package com.test.modules.redeem.service.impl;

import com.test.modules.redeem.bo.RedeemBo;
import com.test.modules.redeem.service.RedeemCodeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * 兑换码生成方案。
 * <p>
 * 兑换码编码特点：
 * - 兑换码具有唯一性
 * - 兑换码尽可能简洁
 * - 兑换码的量级大
 * 由于营运活动的特殊性，要求兑换码能够提前生成，这样可以尽可能的为活动进行预热.
 * 兑换码属于广撒网的策略，所以利用率低，也就不适合使用数据库进行存储.
 * 那么就需要设计一种有效的兑换码生成策略，支持预先生成，支持校验，内容简洁，生成的兑换码都具有唯一性，那么这种策略就是一种特殊的编解码策略，按照约定的编解码规则支撑上述需求
 * </p>
 *
 * <p>
 * 兑换码编码空间：编码空间由字符a-z,A-Z,数字0-9组成，为了增强兑换码的可识别度，剔除大写字母O以及I,可用字符如下所示，共60个字符。为了增加破解难度，打乱字符的顺序。
 * </p>
 * <p>
 * 兑换码组成成分:
 * 优惠方案id(16位) + 兑换码序列号(31位) + 校验码[1的个数(6位) + 余数(7位)]
 * </p>
 *
 * 还可以通过增加字符集数组起点位置标示位的方式，使编码结果更加无序。可参考{@link com.test.modules.token.service.impl.LongEncoder}
 *
 * @author qd-hz
 */
@Service
@Slf4j
public class RedeemCodeServiceImpl implements RedeemCodeService {
    /**
     * 字符集
     */
    private static final char[] DIGIT_MAP = new char[]{
            'L', 'y', 'a', 'n', 'q', 'Q', 'h', '4', 'R', 'M',
            '1', 'W', 'c', 'k', '3', '0', 'D', 'S', '9', 'H',
            'r', 'd', '5', 'g', 'C', 'U', 'p', 'G', 'i', 'P',
            's', 'o', 't', '8', 'b', 'Y', 'N', 'e', '6', 'w',
            'u', 'K', 'x', 'Z', 'A', '2', 'J', 'E', 'v', 'f',
            'j', 'F', '7', 'X', 'B', 'l', 'z', 'm', 'T', 'V'};

    /**
     * 字符集的长度
     */
    private static final long DIGIT_MAP_LENGTH = DIGIT_MAP.length;

    /**
     * 方案ID占用的位数
     */
    private static final int SCHEMA_ID_BIT_LENGTH = 16;
    /**
     * 兑换码序号占用的位数
     */
    private static final int REDEEM_SERIAL_NUM_BIT_LENGTH = 31;
    /**
     * 二进制形式中1的个数占用的位数
     */
    private static final int NUMBER_OF_ONE_BIT_LENGTH = 6;
    /**
     * 余数占用的位数
     */
    private static final int REMAINDER_BIT_LENGTH = 7;
    /**
     * 余数的模
     */
    private static final long DIVISOR = (long) Math.pow(2, REMAINDER_BIT_LENGTH);

    /**
     * 方案ID掩码（用于将其他位置为0）
     */
    private static final long REDEEM_SCHEME_ID_MASK;
    /**
     * 兑换码序列号掩码
     */
    private static final long REDEEM_SERIAL_NUM_MASK;
    /**
     * 二进制形式中1的个数对应的掩码
     */
    private static final long NUMBER_OF_ONE_MASK;
    /**
     * 余数对应的掩码
     */
    private static final long REMAINDER_MASK;

    /** schema ID移动到最右侧需要的位数 */
    private static final short REDEEM_SCHEME_ID_OFFSET_TO_TAIL = REDEEM_SERIAL_NUM_BIT_LENGTH + NUMBER_OF_ONE_BIT_LENGTH + REMAINDER_BIT_LENGTH;
    /** 兑换码序列号移动到最右侧需要的位数 */
    private static final short REDEEM_SERIAL_NUM_OFFSET_TO_TAIL = NUMBER_OF_ONE_BIT_LENGTH + REMAINDER_BIT_LENGTH;
    /** 1的个数移动到最右侧需要的位数 */
    private static final int NUMBER_OF_ONE_OFFSET_TO_TAIL = REMAINDER_BIT_LENGTH;

    // 初始化依赖基础常量的部分常量
    static {
        int size = SCHEMA_ID_BIT_LENGTH + REDEEM_SERIAL_NUM_BIT_LENGTH + NUMBER_OF_ONE_BIT_LENGTH + REMAINDER_BIT_LENGTH;
        int capacity = 64;
        if (capacity < size) {
            throw new RuntimeException("加密串的总长度不能大于64位");
        }
        int schemaIdStart = capacity - size;
        REDEEM_SCHEME_ID_MASK = genMask(schemaIdStart, SCHEMA_ID_BIT_LENGTH);
        int serialNumStart = schemaIdStart + SCHEMA_ID_BIT_LENGTH;
        REDEEM_SERIAL_NUM_MASK = genMask(serialNumStart, REDEEM_SERIAL_NUM_BIT_LENGTH);
        int numberOfOneStart = serialNumStart + REDEEM_SERIAL_NUM_BIT_LENGTH;
        NUMBER_OF_ONE_MASK = genMask(numberOfOneStart, NUMBER_OF_ONE_BIT_LENGTH);
        int reminderStart = numberOfOneStart + NUMBER_OF_ONE_BIT_LENGTH;
        REMAINDER_MASK = genMask(reminderStart, REMAINDER_BIT_LENGTH);
    }

    /**
     * 生成指定连续区间位1的掩码。
     *
     * @param start  起始位置
     * @param length 长度
     * @return 掩码
     */
    private static final long genMask(int start, int length) {
        if (length <= 0) {
            return 0;
        }
        long result = Long.MIN_VALUE;
        result = result >> (length - 1);
        result = result >>> start;
        return result;
    }

    public static void main(String[] args) {
//        RedeemCodeService redeemCodeService = new RedeemCodeServiceImpl();
//
//        RedeemBo redeemBoSource = new RedeemBo(2, 18);
//        String redeemCode = redeemCodeService.getRedeemCode(redeemBoSource.getSchemaId(), redeemBoSource.getSerialNum());
//        RedeemBo redeemBo = redeemCodeService.parseRedeemCode(redeemCode);
//        log.info("Source={}, Target={}", redeemBoSource, redeemBo);
        genRandomCharSequence(60);

    }

    /**
     * 生成随机字符数组，用作编码的字母表
     *
     * @param length 编码空间的长度
     * @return
     */
    public static final String genRandomCharSequence(Integer length) {
        if (length == null) {
            length = 60;
        }
        List<Integer> list = new ArrayList<Integer>(length);
        for (int i = 65; i <= 90; i++) {
            if (i != 73 && i != 79) {
                list.add(i);
            }
        }
        for (int i = 97; i <= 122; i++) {
            list.add(i);
        }
        for (int i = 48; i <= 57; i++) {
            list.add(i);
        }

        for (Integer item : list) {
            System.out.println(item + "-" + (char) (item.intValue()));
        }

        int size = list.size();
        StringBuilder stringBuilder = new StringBuilder();
        boolean useDelimiter = false;
        int formattor = 0;
        while (size > 0) {
            if (useDelimiter) {
                stringBuilder.append(",");
            }
            if (formattor++ % 10 == 0) {
                stringBuilder.append("\n");
            }
            useDelimiter = true;
            int index = (int) (Math.random() * size);
            stringBuilder.append("'").append((char) (list.get(index).intValue())).append("'");
            list.remove(index);
            size = list.size();
        }
        log.info(stringBuilder.toString());
//        System.out.println(stringBuilder);
        return stringBuilder.toString();
    }

    public String getRedeemCode(int redeemSchemaId, int redeemSerialNum) {
        long redeemNum = enRedeemNum(redeemSchemaId, redeemSerialNum);
        log.info("get:redeemNum={}", redeemNum);
        String redeemCode = enRedeemCode(redeemNum);
        log.info("get:redeemCode={}", redeemCode);
        return redeemCode;
    }

    public RedeemBo parseRedeemCode(String redeemCode) {
        long redeemNum = decodeRedeemCode(redeemCode);
        log.info("parse:redeemNum={}", redeemNum);
        if (redeemNum <= 0) {
            return null;
        }

        boolean valid = checkValid(redeemNum);
        log.info("parse:valid={}", valid);

        if (valid) {
            RedeemBo redeemBo = parseRedeemNum(redeemNum);
            log.info("parse:redeemBo={}", redeemBo);
            return redeemBo;
        }

        return null;
    }

    /**
     * 兑换码编号生成算法
     *
     * @param couponSchemeId  优惠方案ID
     * @param redeemSerialNum 兑换码序列号
     * @return
     */
    private long enRedeemNum(long couponSchemeId, long redeemSerialNum) {
        couponSchemeId = couponSchemeId << REDEEM_SERIAL_NUM_BIT_LENGTH;
        long r = couponSchemeId | redeemSerialNum;
        // 1的个数
        long n = numOfOne(r);
        // 余数
        long re = r % DIVISOR;
        r = (r << NUMBER_OF_ONE_BIT_LENGTH) | n;
        r = (r << REMAINDER_BIT_LENGTH) | re;
        return r;
    }

    /**
     * 二进制表示中1的个数
     *
     * @param r
     * @return
     */
    private long numOfOne(long r) {
        int result = 0;

        if (r < 0) {
            result++;
            r = r ^ Long.MIN_VALUE;
        }

        while (r > 0) {
            // 把最后一位的1置为0
            r = r & (r - 1);
            result++;
        }
        return result;
    }

    /**
     * n进制换算以及映射算法
     *
     * @param redeemNum
     * @return
     */
    private String enRedeemCode(long redeemNum) {
        char[] buf = new char[32];
        int charPos = 32;
        while ((redeemNum / DIGIT_MAP_LENGTH) > 0) {
            int ind = (int) (redeemNum % DIGIT_MAP_LENGTH);
            buf[--charPos] = DIGIT_MAP[ind];
            redeemNum /= DIGIT_MAP_LENGTH;
        }
        buf[--charPos] = DIGIT_MAP[(int) (redeemNum % DIGIT_MAP_LENGTH)];
        String str = new String(buf, charPos, (32 - charPos));
        return str;
    }

    /**
     * 解码算法
     *
     * @param redeemNum
     * @return
     */
    private RedeemBo parseRedeemNum(long redeemNum) {
        long redeemCodeSchemeId = redeemNum & REDEEM_SCHEME_ID_MASK;
        long redeemSerialNum = redeemNum & REDEEM_SERIAL_NUM_MASK;
        redeemCodeSchemeId = redeemCodeSchemeId >> REDEEM_SCHEME_ID_OFFSET_TO_TAIL;
        redeemSerialNum = redeemSerialNum >> REDEEM_SERIAL_NUM_OFFSET_TO_TAIL;

        RedeemBo redeemBo = new RedeemBo((int)redeemCodeSchemeId, (int)redeemSerialNum);
        return redeemBo;
    }

    /**
     * 校验算法
     *
     * @param redeemNum
     * @return
     */
    private boolean checkValid(long redeemNum) {
        if (redeemNum > 0) {
            long checkSum = redeemNum & REMAINDER_MASK;
            long numberOfOne = (redeemNum & NUMBER_OF_ONE_MASK) >> NUMBER_OF_ONE_OFFSET_TO_TAIL;
            log.info("valid:numberOfOne = {}", numberOfOne);
            long redeemValue = (redeemNum & (REDEEM_SCHEME_ID_MASK | REDEEM_SERIAL_NUM_MASK)) >> REDEEM_SERIAL_NUM_OFFSET_TO_TAIL;
            log.info("valid:r = {}", redeemValue);
            if (numOfOne(redeemValue) == numberOfOne) {
                if (redeemValue % DIVISOR == checkSum) {
                    return Boolean.TRUE;
                }
            }
        }
        return Boolean.FALSE;
    }

    /**
     * 兑换码解码
     *
     * @param redeemCode
     * @return
     */
    private long decodeRedeemCode(String redeemCode) {
        char chs[] = redeemCode.toCharArray();
        long res = 0L;
        for (int i = 0; i < chs.length; i++) {
            int ind = -1;
            for (int j = 0; j < DIGIT_MAP_LENGTH; j++) {
                if (chs[i] == DIGIT_MAP[j]) {
                    ind = j;
                    break;
                }
            }
            if (ind == -1) {
                return -1;
            }
            if (i > 0) {
                res = res * DIGIT_MAP_LENGTH + ind;
            } else {
                res = ind;
            }
        }
        return res;
    }
}
